/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 */

import path from 'path';
import fs from 'fs';
import {Store, State} from '../reducers';
import {getPluginKey} from './pluginUtils';
import {serialize} from './serialization';

let pluginRecordingState: {
  recording: string;
  pluginName: string;
  startState: any;
  events: [string, any][];
  endState: any;
} = initialRecordingState();

function initialRecordingState(): typeof pluginRecordingState {
  return {
    recording: '',
    startState: undefined,
    events: [],
    endState: undefined,
    pluginName: '',
  };
}

export function flipperRecorderAddEvent(
  pluginKey: string,
  method: string,
  params: any,
) {
  if (pluginRecordingState.recording === pluginKey) {
    pluginRecordingState.events.push([method, params]);
  }
}

async function flipperStartPluginRecording(state: State) {
  if (pluginRecordingState.recording) {
    throw new Error('A plugin recording is already running');
  }
  const app = state.connections.selectedApp;
  const client = state.connections.clients.find((client) => client.id === app);
  if (!app || !client) {
    throw new Error('Can only record plugin states if a device is selected');
  }
  const selectedPlugin = state.connections.selectedPlugin;
  const pluginKey = getPluginKey(client.id, null, selectedPlugin!);
  const plugin = state.plugins.clientPlugins.get(selectedPlugin!);
  if (!selectedPlugin || !plugin) {
    throw new Error('Can only record plugin states if a plugin is selected');
  }
  pluginRecordingState = {
    recording: pluginKey,
    startState: undefined,
    events: [],
    endState: undefined,
    pluginName: selectedPlugin,
  };

  // Note that we don't use the plugin's own serializeState, as that might interact with the
  // device state, and is used for exporting Flipper traces.
  pluginRecordingState.startState = await serialize(
    state.pluginStates[pluginKey] || plugin.defaultPersistedState,
  );

  console.log(
    `Started recordig the states of plugin ${selectedPlugin}..... Use window.flipperStopPluginRecording() to finish this process`,
  );
}

async function flipperStopPluginRecording(state: State) {
  if (!pluginRecordingState.recording) {
    throw new Error('No plugin recording is running. ');
  }
  if (!pluginRecordingState.events.length) {
    console.warn('No events were captured, cancelling recording');
    pluginRecordingState = initialRecordingState();
    return;
  }

  pluginRecordingState.endState = await serialize(
    state.pluginStates[pluginRecordingState.recording],
  );

  const pluginName = pluginRecordingState.pluginName;
  const snapShotFileContents = JSON.stringify(pluginRecordingState);
  const snapShotFileName = `${pluginName}.pluginSnapshot.json`;
  const testFileName = `${pluginName}EventsRunner.tsx`;
  const outDir = getOutputDir(pluginName);

  const testFileContents = generateTestSuite(pluginName, snapShotFileName);

  await fs.promises.writeFile(
    path.join(outDir, snapShotFileName),
    snapShotFileContents,
    'utf8',
  );
  await fs.promises.writeFile(
    path.join(outDir, testFileName),
    testFileContents,
    'utf8',
  );

  console.log(
    `Finished recording ${pluginRecordingState.events.length} for plugin ${
      pluginRecordingState.recording
    }. Generated files ${path.join(outDir, testFileName)} and ${path.join(
      outDir,
      snapShotFileName,
    )}. Move them to the '__tests__ folder of your plugin to incorporate them`,
  );
  pluginRecordingState = initialRecordingState();
}

export function registerRecordingHooks(store: Store) {
  Object.assign(window, {
    flipperStartPluginRecording() {
      flipperStartPluginRecording(store.getState());
    },
    flipperStopPluginRecording() {
      flipperStopPluginRecording(store.getState());
    },
  });
}

function getOutputDir(pluginName: string) {
  const outDir = path.join(process.cwd(), '..');
  const fbPluginDir = path.join(
    outDir,
    'plugins',
    'fb',
    pluginName.toLowerCase(),
    '__tests__',
  );
  const defaultPluginDir = path.join(
    outDir,
    'plugins',
    pluginName.toLowerCase(),
    '__tests__',
  );

  if (fs.existsSync(fbPluginDir)) {
    return fbPluginDir;
  } else if (fs.existsSync(defaultPluginDir)) {
    return defaultPluginDir;
  }
  return outDir;
}

function generateTestSuite(pluginName: string, snapShotFileName: string) {
  return `\
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 */

// This file was initially generated by using \`flipperStartPluginRecording()\` in Flipper console

import fs from 'fs';
import path from 'path';
import {deserialize} from 'flipper';
import Plugin from '../';

test('Verify events produce a consistent end state for plugin ${pluginName}', async () => {
  const snapshotData: {
    startState: string;
    endState: string;
    events: [string, any][];
  } = JSON.parse(
    await fs.promises.readFile(
      path.join(__dirname, '${snapShotFileName}'),
      'utf8',
    ),
  );

  const startState: typeof Plugin.defaultPersistedState = deserialize(
    snapshotData.startState,
  );
  const endState: typeof Plugin.defaultPersistedState = deserialize(
    snapshotData.endState,
  );
  const startTime = Date.now();

  const generatedEndState = snapshotData.events.reduce(
    (store, [method, params]) =>
      Plugin.persistedStateReducer(store, method, params),
    startState,
  );

  const totalTime = Date.now() - startTime;

  expect(generatedEndState).toEqual(endState);
  console.log(
    \`Reducer took $\{totalTime\}ms. to process $\{snapshotData.events.length\} events\`,
  );
});

`;
}
